
var net = require('net');
//var actorify = require('./roleactor.js');
var async = require('async');
var fmt = require('util').format;
var amp = require('amp');
var Message = require('amp-message');
var parse = require('ms');

var MODNAME = 'stackserver';

/**
 * Slice ref.
 */

var slice = [].slice;

/**
 * stacks server object prototype
 *
 * @api private
 */
function StacksServer(pParentObj) {
  var server = {};
  server.backupStacks = [];
  server.clients = {};
  server.reqTimeout = 60000;
   
  // compose MODNAME
  MODNAME = pParentObj._modname + '_' + MODNAME;
  server.logger = pParentObj.logger;
  server.logger.log(MODNAME, 'INFO_LEVEL_4', 'new instance is created');

  
  /**
   * get new clientid
   */
  server.clientId = 30000; //def
  server.getNewClientId = function () {
	return server.clientId++;
  };
  /**
   * get new MsgId
   */
  server.msgId = 1000; //def
  server.getNewMsgId = function () {
	return server.msgId++;
  };
  /**
   * Inspect implementation.
   */
  server.inspect = function(){
    var cbs = Object.keys(server.clients).length;
    return fmt('StacksServer - Clients: %d', cbs);
  };
  /**
   * server connection handler
   * 
   * @api private
   * @param <Socket> sock
   */
  server.connectionHandler = function serverConnectionHandler(sock) {
    
	var handle = {};
	handle.parser = new amp.Stream();
	handle.parser.on('data', handleRcvdMessage.bind(handle));
	sock.pipe(handle.parser);
	handle.stream = sock;

	var clientObj = {
			events: [],
			pendingRequests: {},
			handle: handle
	};
	clientObj.send = sendMessage;
	var clientKey = server.getNewClientId();
	server.clients[clientKey] = clientObj;

	server.logger.log(MODNAME, 'INFO_LEVEL_4', 'client [key: '+clientKey+'] has connected');
       
	 
    
	// send any bcakup stack requests
	for (var i in server.backupStacks) {
		var thisRequest = server.backupStacks[i];
		server.logger.log(MODNAME, 'INFO_LEVEL_4', 'sending backup request [Type: '+thisRequest.type+']');
	
		server.request(thisRequest.type, thisRequest.body, thisRequest.callback);
	
		delete server.backupStacks[i];
	}
   
    
	function sendMessage (type, body, options, callback) {
		var self = this;
		var timer;
		if ('string' != typeof type) throw new Error('missing message type');
		// add request to the clients reqpool
		var rqstObj = {
				type: type,
				body: body,
				callback: callback,
				timer: timer
		};
		server.logger.log(MODNAME, 'INFO_LEVEL_5', 'client [key: '+clientKey+'] Current pendingRequests count: ' + Object.keys(self.pendingRequests).length);
		self.pendingRequests[options._msgId] = rqstObj;
		server.logger.log(MODNAME, 'INFO_LEVEL_5', 'client [key: '+clientKey+'] Added New pending request [MsgId:'+options._msgId+']');
    
		// build msgArgs
		var msgArgs = new Array(3);
		msgArgs[0] = options._msgId;
		msgArgs[1] = type;
		msgArgs[2] = body;

		var msg = new Message(msgArgs);
		self.handle.stream.write(msg.toBuffer());
		server.logger.log(MODNAME, 'INFO_LEVEL_4', 'sendMessage() Sent message: '+JSON.stringify(msg.args));
		
		rqstObj.timer = setTimeout(function(){
			var err = new Error('message response timeout exceeded');
			err.timeout = options._rqstTimeout;
			callback(err);
		}, options._rqstTimeout);
		
		return {
			timeout: function(ms){
				if ('string' == typeof ms) ms = parse(ms);
				timer = setTimeout(function(){
					var err = new Error('message response timeout exceeded');
					err.timeout = ms;
					callback(err);
				}, ms);
			}
		};
	}
    
	function handleRcvdMessage (buf) {
		
		var self = this;
		var msg = new Message(buf);
		server.logger.log(MODNAME, 'INFO_LEVEL_4', 'onMessage() Received: ' + JSON.stringify(msg.args));
		var args = msg.args;
  	  
		// reply message, invoke
		// the given callback
		if ('_reply_' == args[0]) {
			// remove first arg  
			args.shift();
			// get and remove second arg
			var msgId = args.shift().toString();
			// retrieve the clientobj
			var client = server.clients[clientKey];
			// retrieve the request object
			var rqstObj = client.pendingRequests[msgId];
			clearTimeout(rqstObj.timer);
			var fn = rqstObj.callback;
			// delete the pending request as it rcvd the response
			delete client.pendingRequests[msgId];
			
			server.logger.log(MODNAME, 'INFO_LEVEL_5', 'client [key: '+clientKey+'] deleted pending request [MsgId:'+msgId+']');
			// add clientKey to the array
			args.unshift(clientKey);
			
			// execute callback;
			fn.apply(null, args);
		}

    }
    // handle socket close     
   function handleSocketClose() {      
      server.logger.log(MODNAME, 'INFO_LEVEL_4', 'closing client [key: '+clientKey+']s connection');
      if (server.clients[clientKey]) {
    	  var removedClient = server.clients[clientKey];   

    	  /**
           * request (again) any pending requests on disconnected client
           */
         /* for (requestNumber in removedClient.pendingRequests) {
  			server.logger.log(MODNAME, 'INFO_LEVEL_4', 'sending pending request [number: '+requestNumber+']');

        	  var thisRequest = removedClient.pendingRequests[requestNumber];
        	  server.request(thisRequest.type, thisRequest.body, thisRequest.callback);
          }
    	  */
    	  delete server.clients[clientKey];
    	  server.logger.log(MODNAME, 'INFO_LEVEL_4', 'deleted client [key: '+clientKey+']');
      }

     

   };
   sock.on('close', handleSocketClose);
    /**
     * on sock 'error' or 'close', handle socket close
     */
   sock.on('error', function(error) {
	   server.logger.log(MODNAME, 'INFO_LEVEL_4', 'socket error: '+error.message);      
      /*
      if (e.code == ’EADDRINUSE’) {
    	  console.log(’Address in use, retrying...’);
    	  setTimeout(function () {
    	  server.close();
    	  server.listen(PORT, HOST);
    	  }, 1000);
    	  }
      */
      
      return handleSocketClose();
    });  
   
    return clientKey;
  };

  /**
   * listen for client connections
   *
   * @api private
   * @param <Number> port
   * @param <Function> callback
   */
  server.listen = function listen(obj, onStartedListcallback, onNewConCallback) {
	  var connectionListener = null;
	  var host = '127.0.0.1';
	  var port = '';
	  if (typeof obj === 'object') {
		  ipaddr = obj.host;
		  port = obj.port;
	  } else {
		  port = obj;
	  }	  
	  	  
	  server.logger.log(MODNAME, 'INFO_LEVEL_4', 'listen() is called');
	  // validate
	  if (typeof onStartedListcallback != 'function') {
		  onStartedListcallback = function() {};
	  }
	  if (typeof onNewConCallback != 'function') {
		  onNewConCallback = function() {};
	  }
	  // store the callback
	  server.onNewConCallback = onNewConCallback; 
	  // create server
	  connectionListener = net.createServer(function(socket) {
		  var clientKey = server.connectionHandler(socket);
		  onNewConCallback(clientKey);
	  });
	  connectionListener.on('error', function (e) {
		  server.logger.log(MODNAME, 'ERROR', e.code+': '+e.message);		  
	  });
	  connectionListener.listen(port, host, function() {
		  server.logger.log(MODNAME, 'INFO_LEVEL_4', 'server listening on port '+ port);
		  onStartedListcallback();
	  });

  };

 
  /**
   * get the key (actor.id) of the next client to request
   * uses round-robin distribution
   * 
   * @api private
   */
  server.getNextClientKey = function getNextClientKey() {
    var clientKeys = Object.keys(server.clients);
    var clientCount = clientKeys.length;
    
    // choose the client
    var nextClientKey = clientKeys[server.msgId % clientCount];
    server.logger.log(MODNAME, 'INFO_LEVEL_4', 'getNextClientKey: selected client [Key: '+nextClientKey+'] to route the request');

    return nextClientKey;
  };

 

  /**
   * make a request to stacks
   * 
   * @api private
   * @param <String> type
   * @param <Mixed> body
   * @param <Object> options
   * @param <Function> callback
   */
  server.request = function serverRequest(type, body, options, callback) {
   
    server.logger.log(MODNAME, 'INFO_LEVEL_4', 'request() is called');
   
    if (typeof options === 'function') {
      callback = options;
      options = {};
    }

    if (typeof options.maxRetries === 'undefined') {
      options.maxRetries = 3; //def
    }
    
    if (typeof options._retrySequence === 'undefined') {
      options._retrySequence = 0; //def
    }
    if (typeof options._reqTimeout === 'undefined') {
        options._reqTimeout = server.reqTimeout; //def
      }
    // if no clients are connected, add request to backup stacks and return    
    if (Object.keys(server.clients).length < 1) {
    	server.logger.log(MODNAME, 'INFO_LEVEL_4', 'no client found active, backing up request');
      
    	server.backupStacks.push({
	        type: type,
	        body: body,
	        callback: callback
    	});
    	
      return;
    }
    // get new msgid
    options._msgId = server.getNewMsgId();
    server.logger.log(MODNAME, 'INFO_LEVEL_4', 'Message Id: '+ options._msgId);
    /**
     * if options._retrySequence++ >= options.maxRetries, 
     *  request
     * else
     *  callback with new error
     */
   
    if (options._retrySequence++ <= options.maxRetries) {
      var clientKey = clientKey = server.getNextClientKey();
      var clientObj = server.clients[clientKey];
      
      var x = clientObj.send(type, body, options, function(clientId, error, response) {
    	 if (error) {
    		 server.logger.log(MODNAME, 'INFO_LEVEL_4', 'request [msgId: '+options._msgId+'] resulted in error: '+ JSON.stringify(error));
    		 setTimeout(server.request(type, body, options, callback),10);
        } else {
        	 server.logger.log(MODNAME, 'INFO_LEVEL_4', 'request [msgId: '+options._msgId+'] had response: '+ JSON.stringify(response));
        	 callback(error, response, clientId);
        }
      });
      
    } else {
      callback(new Error(type +' request failed after max retry attempts'), null, clientKey);
    }

  };

  
  
   
  /**
   * expose public api
   */
  this.listen = server.listen;
  this.request = server.request;

  return this;
};



module.exports = StacksServer;